41 KMP子串查找算法
KMP子串查找算法(一)
-
问题 如何在目标字符串S中,查找是否存在子串P?
-
朴素解法
int sub_str_index(const char *s, const char *p) {
int ret = -1;
int sl = strlen(s);
int pl = strlen(p);
int len = sl - pl;
for (int i = 0; (ret < 0) && (i <= len); i++) {
bool equal = true;
for (int j = 0; equal && (j < pl); j++)
equal = equal && (s[i + j] == p[j]);
ret = (equal ? i : -1);
}
return ret;
} -
朴素解法的一个优化线索
因为,pa != pb,且pb == sb; 所以,pa != sb; 结论,子串p右移1位比较,没有意义。
-
示例
-
伟大的发现
- 匹配失败时的右移位数与子串本身相关,与目标串无关
- 移动位数 = 已匹配的字数-对应的部分匹配值
- 任意子串都存在一个唯一的部分匹配表
-
部分匹配表示例
Partial Matched Table
1 2 3 4 5 6 7 A B C D A B D 0 0 0 0 1 2 0 用法:
第7位匹配失败→前6位匹配成功→查表PMT[6]→右移位数6-PMT[6]=6-2=4
-
问题 部分匹配表是怎么得到的?
-
前缀
- 除了最后一个字符以外,一个字符串的全部头部组合
-
后缀
- 除了第一个字符以外,一个字符串的全部尾部组合
-
部分匹配值
- 前缀和后缀最长共有元素的长度
-
示例:ABCDABD
- | 字符 | 前缀 | 后缀 | 交集 | 匹配值 |
---|---|---|---|---|---|
1 | A | 空 | 空 | 空 | 0 |
2 | AB | A | B | 空 | 0 |
3 | ABC | A,AB | BC,C | 空 | 0 |
4 | ABCD | A,AB,ABC | BCD,CD,D | 空 | 0 |
5 | ABCDA | A,AB,ABC,ABCD | BCDA,CDA,DA,A | A | 1 |
6 | ABCDAB | A,AB,ABC,ABCD,ABCDA | BCDAB,CDAB,DAB,AB,B | AB | 2 |
7 | ABCDABD | A,AB,ABC,ABCD,ABCDA,ABCDAB | BCDABD,CDABD,DABD,ABD,BD,D | 空 | 0 |
-
问题 怎么编程产生部分匹配表?
-
实现关键
- PMT[1] = 0(下标为0的元素匹配值为0)
- 从2个字符开始递推(从下标为1的字符开始递推)
- 假设PMT[n]=PMT[n-1]+1(最长共有元素的长度)
- 当假设不成立,PMT[n]在PMT[n-1]的基础上减小
编程实验(一)
-
部分匹配表的递推与实现
size_t* make_pmt(const char *p){
auto len = strlen(p);
auto *ret = reinterpret_cast<size_t*>(malloc(sizeof (size_t)*len));
if(ret!=nullptr){
size_t ll=0;
ret[0] = 0;
for (size_t i=1;i<len;i++) {
while ((p[ll]!=p[i])&&(ll>0)){
ll = ret[ll-1];
}
if(p[ll]==p[i]) ll++;
ret[i]=ll;
}
}
return ret;
}
KMP子串查找算法(二)
-
部分匹配表的使用(KMP算法)
下标j处匹配失败→前j位匹配成功→查表PMT[j-1]→右移位数j-PMT[j-1]
因为,s[i]!=p[j]
所以,查表,LL=PMT[j-1]
于是,右移,i的值不变,j的值改变,j=j-(j-LL)=LL=PMT[j-1];
编程实验(二)
-
KMP子串查找算法的实现
int kmp(const char*s,const char*p){
int ret = -1;
auto sl = strlen(s);
auto pl = strlen(p);
auto pmt = make_pmt(p);
if((pmt!=nullptr)&&(0<pl)&&(pl<=sl)){
for (size_t i=0,j=0;i<sl;i++) {
while ((j>0)&&(s[i]!=p[j])) {
j=pmt[j-1];
}
if(s[i]==p[j]) j++;
if(j==pl) ret = static_cast<int>(i+1-pl);
}
}
free(pmt);
return ret;
}
小结
- 部分匹配表是提高子串查找效率的关键
- 部分匹配值定义为前缀和后缀最长共有元素的长度
- 可以用递推的方法产生部分匹配表
- KMP利用部分匹配表与子串移动位数的关系提高查找效率